package top.ruanlitao.shortlink.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.ruanlitao.shortlink.admin.common.biz.user.UserContext;
import top.ruanlitao.shortlink.admin.common.constant.RedisCacheConstant;
import top.ruanlitao.shortlink.admin.common.convention.exception.ClientException;
import top.ruanlitao.shortlink.admin.common.enums.UserErrorCodeEnum;
import top.ruanlitao.shortlink.admin.dao.entity.UserDO;
import top.ruanlitao.shortlink.admin.dao.mapper.UserMapper;
import top.ruanlitao.shortlink.admin.dto.biz.UserLoginInfoDTO;
import top.ruanlitao.shortlink.admin.dto.req.UserLoginReqDTO;
import top.ruanlitao.shortlink.admin.dto.req.UserRegisterReqDTO;
import top.ruanlitao.shortlink.admin.dto.req.UserUpdateReqDTO;
import top.ruanlitao.shortlink.admin.dto.resp.UserLoginRespDTO;
import top.ruanlitao.shortlink.admin.dto.resp.UserRespDTO;
import top.ruanlitao.shortlink.admin.service.GroupService;
import top.ruanlitao.shortlink.admin.service.UserService;
import top.ruanlitao.shortlink.admin.toolkit.ShortLinkUtil;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 用户接口实现层
 */
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {

    // 布隆过滤器
    private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter;
    // Redisson 客户端
    private final RedissonClient redissonClient;
    // Redis 客户端
    private final StringRedisTemplate stringRedisTemplate;
    // 分组服务
    private final GroupService groupService;

    @Override
    public UserRespDTO getUserByUsername(String username) {
        // 构造查询 wrapper
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers
                .lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, username);
        // 直接通过 mybatis-plus 提供的 baseMapper 对象查询
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        // 结果为空 抛出自定义异常
        if (userDO == null) {
            throw new ClientException(UserErrorCodeEnum.USER_NULL);
        }
        // 参数拷贝
        UserRespDTO result = new UserRespDTO();
        BeanUtils.copyProperties(userDO, result);
        return result;
    }

    @Override
    public Boolean hasUsername(String username) {
        // 从布隆过滤器中查找，
        // 如果布隆过滤器返回 false，代表数据库中一定没有这个用户名，
        // 如果返回 true，代表数据库中可能没有这个用户名，因为存在误判，但在该业务中，这个误判是可以接受的，所以不对返回 true 的情况做特殊处理
        return userRegisterCachePenetrationBloomFilter.contains(username);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void register(UserRegisterReqDTO requestParam) {
        // 判断用户名是否存在
        if (hasUsername(requestParam.getUsername())) {
            // 用户名已存在，直接返回
            throw new ClientException(UserErrorCodeEnum.USER_NAME_EXIST);
        }
        // 根据用户名获取到分布式锁
        RLock lock = redissonClient.getLock(RedisCacheConstant.LOCK_USER_REGISTER_KEY + requestParam.getUsername());
        // 尝试获取锁，没有获取到锁的直接抛异常
        if (!lock.tryLock()) {
            // 没有获取到锁的直接返回错误信息
            throw new ClientException(UserErrorCodeEnum.USER_NAME_EXIST);
        }
        try {
            // 插入用户数据
            int count = baseMapper.insert(BeanUtil.toBean(requestParam, UserDO.class));
            // 判断插入成功数据条数
            if (count < 1) {
                throw new ClientException(UserErrorCodeEnum.USER_SAVE_ERROR);
            }
            // 创建一个默认分组
            groupService.saveGroup(requestParam.getUsername(), "默认分组");
            stringRedisTemplate.opsForValue().set(String.format(RedisCacheConstant.GROUP_COUNT_KEY, requestParam.getUsername()), "1");
            // 注册成功，将 username 加入到布隆过滤器
            userRegisterCachePenetrationBloomFilter.add(requestParam.getUsername());
        } catch (DuplicateKeyException ex) {
            // 插入数据库出错（用户名记录已存在）
            throw new ClientException(UserErrorCodeEnum.USER_EXIST);
        } finally {
            // 最后释放锁
            lock.unlock();
        }
    }

    @Override
    public void update(UserUpdateReqDTO requestParam) {
        // 验证当前用户的用户名是否是登录用户
        if (!Objects.equals(requestParam.getUsername(), UserContext.getUsername())) {
            throw new ClientException(UserErrorCodeEnum.USER_UPDATE_ERROR);
        }
        LambdaUpdateWrapper<UserDO> updateWrapper = Wrappers
                .lambdaUpdate(UserDO.class)
                .eq(UserDO::getUsername, requestParam.getUsername());
        int update = baseMapper.update(BeanUtil.toBean(requestParam, UserDO.class), updateWrapper);
        if (update != 1) {
            throw new ClientException(UserErrorCodeEnum.USER_UPDATE_ERROR);
        }
    }

    @Override
    public UserLoginRespDTO login(UserLoginReqDTO requestParam, ServletRequest request) {
        // 根据用户名查看缓存，判断用户是否登录（允许用户可以多端登录）
        Map<Object, Object> hasLoginMap = stringRedisTemplate.opsForHash().entries(RedisCacheConstant.USER_LOGIN_KEY + requestParam.getUsername());
        if (CollUtil.isNotEmpty(hasLoginMap)) {
            String token = ((HttpServletRequest) request).getHeader("token");
            if (!token.isBlank()) {
                Set<String> collect = hasLoginMap.keySet().stream()
                        .filter(key -> key.equals(token))
                        .map(Object::toString)
                        .collect(Collectors.toSet());
                if (collect.size() == 1) {
                    return UserLoginRespDTO.builder().token(token).build();
                }
            } else {
                // 限制最多可以登录 5 个客户端
                if (hasLoginMap.size() >= 5) {
                    throw new ClientException(UserErrorCodeEnum.USER_LOGGED_DEVICE_UPPER_LIMIT);
                }
            }
        }
        // 下面的代码只能实现一端登录
        /*Boolean hasLogin = stringRedisTemplate.hasKey(RedisCacheConstant.USER_LOGIN_KEY + requestParam.getUsername());
        if (Boolean.TRUE.equals(hasLogin)) {
            throw new ClientException(UserErrorCodeEnum.USER_ALREADY_LOGIN);
        }*/
        // 根据输入的用户名和密码在数据库中查找用户
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, requestParam.getUsername())
                .eq(UserDO::getPassword, requestParam.getPassword());
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        if (userDO == null) {
            throw new ClientException(UserErrorCodeEnum.USER_LOGIN_ERROR);
        }
        // 生成登录 token 信息，将 token 信息最为 key 保存到缓存
        String token = UUID.randomUUID().toString();
        // 生成登录 用户 信息，将用户信息作为 value 保存到缓存
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        UserLoginInfoDTO userLoginInfoDTO = UserLoginInfoDTO.builder()
                .id(userDO.getId())
                .username(requestParam.getUsername())
                .realName(userDO.getRealName())
                .loginDateTime(LocalDateTime.now())
                .loginBrowser(ShortLinkUtil.getBrowser(httpServletRequest))
                .loginIp(ShortLinkUtil.getActualIp(httpServletRequest))
                .build();
        stringRedisTemplate.opsForHash().put(RedisCacheConstant.USER_LOGIN_KEY + requestParam.getUsername(), token, JSON.toJSONString(userLoginInfoDTO));
        // TODO 为了后续测试方便，暂时将登录过期时间设置为为 30 天
        stringRedisTemplate.expire(RedisCacheConstant.USER_LOGIN_KEY + requestParam.getUsername(), 30L, TimeUnit.DAYS);
        // 返回登录 token
        return UserLoginRespDTO.builder().token(token).build();
    }

    @Override
    public Boolean checkLogin(String username, String token) {
        String userLoginInfoDTOJSON = (String) stringRedisTemplate.opsForHash().get(RedisCacheConstant.USER_LOGIN_KEY + username, token);
        UserLoginInfoDTO userLoginInfoDTO = JSON.parseObject(userLoginInfoDTOJSON, UserLoginInfoDTO.class);
        if (userLoginInfoDTO != null) {
            return username.equals(userLoginInfoDTO.getUsername());
        }
        return false;
    }

    @Override
    public void logout(String username, String token) {
        // 检查用户是否已经登录
        if (!checkLogin(username, token)) {
            throw new ClientException(UserErrorCodeEnum.USER_NOT_LOGIN);
        }
        // 去除 redis 中的 token
        stringRedisTemplate.opsForHash().delete(RedisCacheConstant.USER_LOGIN_KEY + username, token);
    }

}
